package com.anti.jdbc.support.mapper;
import com.anti.commom.core.exception.DatabaseException;
import com.anti.commom.core.utils.ClassUtils;
import com.anti.commom.core.utils.StringUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.support.JdbcUtils;

import javax.persistence.Column;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.sql.*;
import java.util.Arrays;
import java.util.Date;

/**
 * @Description TODO
 * @Author anTi
 * @Date 2020-10-6
 */
public class ResultSetToBeanProcess {
    private static Logger logger = LoggerFactory.getLogger(ResultSetToBeanProcess.class);
    protected static final int COLUMN_NOT_FOUND = -1;
    private int[] propertyToColumn;

    public ResultSetToBeanProcess() {
    }

    public <T> T toBean(ResultSet rs, Class<T> clazz) throws SQLException {
        PropertyDescriptor[] props = PropertyUtils.getPropertyDescriptors(clazz);
        ResultSetMetaData rsmd = rs.getMetaData();
        if (this.propertyToColumn == null) {
            this.mapColumnsToProperties(rsmd, props);
        }

        T bean = ClassUtils.getInstance(clazz);

        for(int i = 0; i < this.propertyToColumn.length; ++i) {
            if (this.propertyToColumn[i] != -1) {
                PropertyDescriptor prop = props[i];
                Method setter = prop.getWriteMethod();
                Class<?> propType = setter.getParameterTypes()[0];
                if (propType.isPrimitive()) {
                    propType = ClassUtils.primitiveToWrapper(propType);
                }

                Object value = this.processColumn(rs, this.propertyToColumn[i], propType);
                String keyf;
                if (value instanceof Date) {
                    keyf = propType.getName();
                    value = this.convertValueByType(value, keyf);
                }
                try {
                    setter.invoke(bean, value);
                } catch (InvocationTargetException var13) {
                    throw new DatabaseException("Cannot set Value[" + value + "] to Property[" + prop.getName() + "]", var13);
                } catch (IllegalAccessException var14) {
                    throw new DatabaseException("Cannot set Value[" + value + "] to Property[" + prop.getName() + "]", var14);
                }
            }
        }

        return bean;
    }

    private Object convertValueByType(Object value, String typeName) {
        if ("java.sql.Date".equals(typeName)) {
            return new java.sql.Date(((Date)value).getTime());
        } else if ("java.sql.Time".equals(typeName)) {
            return new Time(((Date)value).getTime());
        } else if ("java.sql.Timestamp".equals(typeName)) {
            Timestamp tsValue = (Timestamp)value;
            int nanos = tsValue.getNanos();
            Object values = new Timestamp(tsValue.getTime());
            ((Timestamp)values).setNanos(nanos);
            return values;
        } else {
            return value;
        }
    }

    public <T> T toPrimitive(ResultSet rs, Class<T> clazz) throws SQLException {
        Object value = this.processColumn(rs, 1, clazz);
        if (value instanceof Date) {
            String typeName = clazz.getName();
            value = this.convertValueByType(value, typeName);
        }
        return (T) value;
    }

    protected Object processColumn(ResultSet rs, int index, Class<?> propType) throws SQLException {
        if (!propType.isPrimitive() && rs.getObject(index) == null) {
            return null;
        } else if (propType.equals(String.class)) {
            return rs.getString(index);
        } else if (propType.equals(Integer.class)) {
            return rs.getInt(index);
        } else if (propType.equals(Long.class)) {
            return rs.getLong(index);
        } else if (propType.equals(Float.class)) {
            return rs.getFloat(index);
        } else if (propType.equals(Double.class)) {
            return rs.getDouble(index);
        } else if (propType.equals(BigDecimal.class)) {
            return rs.getBigDecimal(index);
        } else if (propType.equals(Boolean.class)) {
            return rs.getBoolean(index);
        } else if (propType.equals(Short.class)) {
            return rs.getShort(index);
        } else if (propType.equals(Byte.class)) {
            return rs.getByte(index);
        } else if (propType.equals(Timestamp.class)) {
            return rs.getTimestamp(index);
        } else if (propType.equals(Time.class)) {
            return rs.getTime(index);
        } else {
            return propType.equals(SQLXML.class) ? rs.getSQLXML(index) : rs.getObject(index);
        }
    }

    protected void mapColumnsToProperties(ResultSetMetaData rsmd, PropertyDescriptor[] props) throws SQLException {
        int propCount = props.length;
        this.propertyToColumn = new int[propCount];
        Arrays.fill(this.propertyToColumn, -1);

        for(int i = 0; i < propCount; ++i) {
            String propName = props[i].getName();
            if (!"class".equals(propName)) {
                Method setter = props[i].getWriteMethod();
                if (setter == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug(String.format("not setter of property %s", propName));
                    }
                } else {
                    Method getter = props[i].getReadMethod();
                    boolean isColumnDefined = false;
                    if (getter != null) {
                        Column column = (Column)getter.getAnnotation(Column.class);
                        if (column != null && StringUtils.isNotBlank(column.name())) {
                            propName = column.name();
                            isColumnDefined = true;
                        }
                    }
                    int colCount = rsmd.getColumnCount();

                    for(int k = 1; k <= colCount; ++k) {
                        String colName = JdbcUtils.lookupColumnName(rsmd, k);
                        if (!isColumnDefined) {
                            colName = StringUtils.remove(colName, '_');
                        }

                        if (propName.equalsIgnoreCase(colName)) {
                            this.propertyToColumn[i] = k;
                            break;
                        }
                    }
                }
            }
        }

    }
}
